package cn.okcode.api.framework.js;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.okcode.api.framework.Config;
import cn.okcode.api.framework.Jsp2ApiException;
import cn.okcode.api.framework.annotation.CPAction;
import cn.okcode.api.framework.annotation.CPParam;
import cn.okcode.api.framework.annotation.CPService;
import cn.okcode.api.framework.result.ResultHandlerEnum;
import cn.okcode.api.framework.annotation.CPController;
import cn.okcode.api.framework.dispatcher.JspApiDispatcher;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

/**
 * js调用端代码生成服务
 *
 * @author pyk13
 */
@CPService
public class JsService {
    public static final Logger logger = LogManager.getLogger(JsService.class.getName());

    public static final String MOUDLE = "CAL";

    public static final String JS_CONTROLLER_PACKAGE = "cn.okcode.mvc.v2.framework.js";

    public static final String JS_CALL_NAME = "jsCallName";
    public static final String AJAX_TIMEOUT = "ajaxTimeout";

    private static String jsCalleeString = "";

    public static void refresh() {
        jsCalleeString = "";
    }

    public String build() {
        if (StringUtils.isBlank(jsCalleeString)) {
            if (logger.isDebugEnabled()) {
                logger.debug("准备数据 - prepare");
            }
            List<JController> controllers = prepare();
            InnerStringBuilder builder = new InnerStringBuilder();
            builder.append("window.cal = window.CAL ={");
            char comma = ' ';

            if (logger.isDebugEnabled()) {
                logger.debug("开始构建");
            }
            for (JController controller : controllers) {
                builder.append(comma);
                controller.build(builder);
                comma = ',';
            }
            if (logger.isDebugEnabled()) {
                logger.debug("完成构建");
            }
            builder.appendLine("}");
            jsCalleeString = builder.toString();
        }
        return jsCalleeString;
    }

    private List<JController> prepare() {
        //获取所有类
        StopWatch stopWatch = new StopWatch();
        List<JController> controllers = new ArrayList<>();
        String jarPath = this.getClass().getProtectionDomain().getCodeSource().getLocation().getFile();

        Set<Class<?>> classes = new HashSet<>();
        if (jarPath != null && jarPath.endsWith(".jar")) {
            stopWatch.start("doJarScanner--jarPath--" + jarPath);
            classes.addAll(doJarScanner(jarPath, Config.CONTROLLER_PACKAGE_NAME));
            stopWatch.stop();
        } else {
            for (String packageName : Config.CONTROLLER_PACKAGE_NAME) {
                if (logger.isDebugEnabled()) {
                    logger.debug("packageName:" + packageName);
                }
                stopWatch.start("prepare--scanPackageByAnnotation--" + packageName);
                classes.addAll(ClassUtil.scanPackageByAnnotation(packageName, CPController.class));
                stopWatch.stop();
            }
        }
        stopWatch.start("prepare--new JController");
        for (Class<?> clazz : classes) {
            controllers.add(new JController(clazz));
        }
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
        return controllers;
    }

    private Set<Class<?>> doJarScanner(String jarPath, String... scanPackages) {
        if (logger.isDebugEnabled()) {
            logger.debug("jarPath:" + jarPath);
            logger.debug("scanPackages:" + StringUtils.join(scanPackages, ","));
        }

        Set<Class<?>> classSet = new HashSet<>();

        final String CLASS_SUFFIX = ".class";
        File jarFile = new File(jarPath);
        try (JarFile jar = new JarFile(jarFile)) {
            Enumeration<JarEntry> entries = jar.entries();
            int count = 0;
            while (entries.hasMoreElements()) {
                count++;
                JarEntry entry = entries.nextElement();
                String entryName = entry.getName().replaceAll("/", ".");
                if (!entryName.endsWith(CLASS_SUFFIX)) {
                    continue;
                }
                for (String packageName : scanPackages) {
                    if (entryName.startsWith(packageName)) {
                        // 在这里处理找到的类文件
                        String className = entryName.substring(0, entryName.length() - CLASS_SUFFIX.length());
                        if (logger.isDebugEnabled()) {
                            logger.debug("entryName: " + entryName);
                            logger.debug("className: " + className);
                        }
                        Class<?> clazz = Class.forName(className);
                        Annotation annotation = AnnotationUtil.getAnnotation(clazz, CPController.class);
                        if (annotation != null) {
                            classSet.add(clazz);
                        }
                    }
                }
            }
            if (logger.isDebugEnabled()) {
                logger.debug("count:" + count);
            }
        } catch (IOException | ClassNotFoundException e) {
            throw new Jsp2ApiException("打开jar包出错,jar路径：" + jarPath);
        }
        return classSet;
    }

    private static String default2String(Object value) {
        if (value == null) {
            return "";
        }
        return value.toString();
    }

    private static class JController {
        private final List<JAction> actions;
        private final String invokerName;
        private final String controllerName;

        JController(Class<?> clazz) {
            Method[] methods = ReflectUtil.getMethods(clazz, method -> null != AnnotationUtil.getAnnotation(method, CPAction.class));
            String simpleClassName = clazz.getSimpleName();
            int classNameLen = simpleClassName.length();
            controllerName = simpleClassName.substring(0, classNameLen - 10); //"Controller".length=10
            invokerName = controllerName;
            actions = new ArrayList<>();
            for (Method method : methods) {
                actions.add(new JAction(method, invokerName));
            }
            namedActionJsName();
        }

        /**
         * 重命名
         */
        void namedActionJsName() {
            //按照签名排序，保证重载方法的序号重视一致
            List<JAction> namedActions = actions.stream().filter(action -> action.jsName.isEmpty())
                    .sorted(Comparator.comparing(JAction::getSignature)).collect(Collectors.toList());
            int repeatTimes = 0;
            String lastActionName = "";
            for (JAction action : namedActions) {
                if (lastActionName.equals(action.name)) {
                    repeatTimes++;
                } else {
                    repeatTimes = 0;
                }
                lastActionName = action.name;
                if (repeatTimes > 0) {
                    action.jsName = action.name + repeatTimes;
                } else {
                    action.jsName = action.name;
                }
            }
        }


        void build(InnerStringBuilder output) {
            output.append(invokerName).appendLine(" : {");
            output.append("controller:'").append(controllerName).appendLine("',");
            char comma = ' ';
            for (JAction action : actions) {
                output.append(comma);
                comma = ',';
                action.build(output);
                output.append(comma);
                action.buildAsync(output);
            }
            output.append("}");
        }
    }

    private static class JAction {
        private final Method method;
        private String jsName = "";
        private final String name;
        private final String invokerName;

        private final Parameter[] parameters;
        private List<String> parameterNames;

        //方法结果处理方式，RAW - 返回string STATUS - 返回Result结构，这里不需要关心具体有哪些情况
        private String resultHandlerKind;

        //ajax请求超时时间
        private int ajaxTimeout = 30000;

        JAction(Method method, String invokerName) {
            this.method = method;
            this.parameters = method.getParameters();
            this.name = method.getName();
            this.invokerName = MOUDLE + "." + invokerName;

            initialize();
        }

        String getSignature() {
            return String.format("%s.%s", method.getName(),
                    Arrays.stream(parameters).map(p -> p.getType().getName())
                            .collect(Collectors.joining(".")));
        }

        void build(InnerStringBuilder output) {
            output.append(jsName).append(":function(");
            if (parameterNames.size() > 0) {
                output.append(StringUtils.join(parameterNames, ",")).appendLine(",okCb,errCb)");
            } else {
                output.appendLine("okCb,errCb)");
            }
            String ajaxMethod = "invoke" + resultHandlerKind + "Sync";
            output.appendLine("{")
                    .appendLine("return ajaxUtil.").append(ajaxMethod).append("({")
                    .append("_controller: ").append(invokerName).appendLine(".controller,")
                    .append("_action:'").append(name).appendLine("',")
                    .append("_ajaxTimeout:").append(String.valueOf(ajaxTimeout)).appendLine(",")
                    .append(createParameterKV())
                    .appendLine(createDefOfParameters())
                    .append("},okCb,errCb);")
                    .appendLine("}");
        }

        void buildAsync(InnerStringBuilder output) {
            output.append(jsName).append("Async")
                    .append(":function(");
            if (parameterNames.size() > 0) {
                output.append(StringUtils.join(parameterNames, ",")).appendLine(",okCb,errCb)");
            } else {
                output.appendLine("okCb,errCb)");
            }
            String ajaxMethod = "invoke" + resultHandlerKind;
            output.append("{")
                    .appendLine("return ajaxUtil.").append(ajaxMethod).append("({")
                    .append("_controller: ").append(invokerName).appendLine(".controller,")
                    .append("_action:'").append(name).appendLine("',")
                    .append("_ajaxTimeout:").append(String.valueOf(ajaxTimeout)).appendLine(",")
                    .appendLine(createParameterKV())
                    .appendLine(createDefOfParameters())
                    .append("},okCb,errCb);")
                    .appendLine("}");
        }

        void initialize() {
            parameterNames = new ArrayList<>();
            for (Parameter parameter : parameters) {
                String parameterName = default2String(AnnotationUtil.getAnnotationValue(parameter, CPParam.class));
                parameterNames.add(parameterName);
            }

            Map<String, Object> values = AnnotationUtil.getAnnotationValueMap(method, CPAction.class);
            String resultHandler = default2String(values.get(JspApiDispatcher.RESULT_HANDLER_STR));
            ResultHandlerEnum handlerEnum = ResultHandlerEnum.valueOf(resultHandler);
            this.resultHandlerKind = handlerEnum.equals(ResultHandlerEnum.RAW) ? "Raw" : "";

            String jsCallName = default2String(values.get(JS_CALL_NAME));
            if (!"".equals(jsCallName)) {
                this.jsName = jsCallName; //用于重载方法名
            }

            this.ajaxTimeout = Integer.parseInt(values.get(AJAX_TIMEOUT).toString());
        }

        /**
         * _defOfParameters: JSON.stringify([
         * {
         * name: 'oid',
         * type: 'java.lang.String'
         * },
         * ])
         *
         * @return
         */
        String createDefOfParameters() {
            if (parameters.length == 0) {
                return "";
            }
            InnerStringBuilder def = new InnerStringBuilder();
            def.appendLine("_parameters: [");
            char comma = ' ';
            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                def.append(comma);
                comma = ',';
                def.appendLine("{");
                def.append("name").append(":'").append(parameterNames.get(i)).appendLine("'");
                if (!String.class.equals(parameter.getType())) {
                    String typename = parameter.getType().getName();
                    def.append(",type").append(":'").append(typename).appendLine("'");
                }
                def.appendLine("}");
            }
            def.appendLine("]");
            return def.toString();
        }

        String createParameterKV() {
            InnerStringBuilder kv = new InnerStringBuilder();
            for (String parameterName : parameterNames) {
                kv.append(parameterName).append(":").appendLine(parameterName);
                kv.append(',');
            }
            return kv.toString();
        }
    }

    private static class InnerStringBuilder {
        StringBuilder builder;

        InnerStringBuilder() {
            builder = new StringBuilder();
        }

        InnerStringBuilder append(String str) {
            builder.append(str);
            return this;
        }

        InnerStringBuilder append(char chr) {
            builder.append(chr);
            return this;
        }

        InnerStringBuilder appendLine(String str) {
            builder.append(str);
            appendLine();
            return this;
        }

        InnerStringBuilder appendLine() {
            builder.append(System.lineSeparator());
            return this;
        }

        @Override
        public String toString() {
            return builder.toString();
        }
    }
}
